Repository navigation
wpfacts, codegen: arrays held in class fields read under Rep::Arr, and field read calls inside the floor (P4 lever 1, PR-C3) - #14402
Merged
Merged
Conversation
added 25 commits
November 9, 2026 20:17
An Array's length interval keeps its birth `lo`; growth raises only `hi` (solve/arr_len.rs, DESIGN-p4l1 §2.5): - an index store with key interval [a, b], b >= 0, raises hi to min(b + 1, u32::MAX); - a store whose keys are all negative changes nothing (a negative key is a named property in the separate bag); - an append (push, an element store at a non-index site) raises hi to u32::MAX: a rule runs once per token, not per runtime append; - reverse/copyWithin make the length ⊤ once hi > lo (Cons::LenPermuted, a monotone rule on the length node); the generic `.call` forms make it ⊤ outright; - every shrinking event stays ⊤. A `new Array(k)` birth length is recorded only for k <= MAX_FRESH_DENSE_ARRAY_LENGTH, so the birth guarantees capacity >= floor, and the element read's consumer-side capacity test is removed; the witness keeps checking capacity >= len. The inline literal header goes through perry_abi::array_birth_header, which asserts capacity >= length. A runtime test classifies every `capacity` write (incl. storage.rs's empty-queue reset); the stale expando comments are fixed. PERRY_FACTS_ARRAY_FLOOR=0 restores the previous transfer (a compile-cache input). Sabotage arms arrfloor, arrappend, arrcap (alias densebirth) and arrfwd with S0 fixtures; node-exact parity fixtures for growth, shrink and permutation.
A binding or class field that holds only plain program Arrays gets its own
representation, Rep::Arr(ArrRep { elem, floor, dense, fixed })
(DESIGN-p4l1 §2.1-2.6). No consumer yet.
- TmplKind::Arr gains `plain`: set only at an array literal, a spread
literal and new Array/Array() (site_new_array). JSON arrays, stand-ins and
every runtime-born array are not plain.
- R1: the per-site template key holds `plain`, so a call whose callee holds
both Array and JSON.parse births one template per kind; only a plain birth
ever makes or finds a plain template, and only it carries the new Array
birth length.
- R2: BirthKind::Array carries `plain` beside `hole_free`, so a body
packet's birth names it. The fused-fill mark lives in arr_len_at_site's
value (SiteBirth { len, hole_free }), not in a new map.
- R3: ArrRep carries `fixed` (every token's length interval is one value:
never grows, never forwarded), joined by AND, so a field or a signature
slot keeps or drops it with the rest and no consumer asks const_length.
- new Array(k).fill(v) (one argument, the fresh receiver) is born
hole-free: the intrinsic lock makes it the native fill.
- Rep loses its derived order; Rep::join is one full table (F64/I32 join
Arr to Boxed) and Rep::encoding() gives the slot encoding (Arr: Boxed).
P2's local join is deleted; Signature::is_boxed, admits and every
codegen signature decision go through the encoding, so an Arr slot never
makes a function typed or a site untyped.
- FieldFacts::rep: Arr of the field over every token when definite, data,
not escaped and not hard; joined (not met) over classes.
- Dump: `# arr_rep` per binding, `FACT FR rep` per field.
- Check mode: Fact::ArrayRep at an element read of an Arr local (band,
GC_TYPE_ARRAY, not forwarded when fixed, length and capacity >= floor,
no hole below the floor when dense, element kind of a present slot).
- PERRY_FACTS_ARRAY_REPS=0 turns it off (a compile-cache input). Sabotage
arms arrdense, arrelem, arrlazy with S0 fixtures; parity fixtures for
births/fused fill and lazy/runtime-born arrays; wpfacts unit tests.
…EPS=0 With Rep::Arr on, the array rep witness reports the false length of the densebirth/arrcap arms first (a correct detection under another predicate); each older arm now reaches the witness it was written for.
…'s lazy-header threshold)
# Conflicts: # crates/perry-codegen/src/expr/index_get.rs # crates/perry-wpfacts/src/lib.rs # crates/perry-wpfacts/src/model/cons.rs # crates/perry/src/commands/compile/program_facts.rs
NUM_SEEDS.md: rows for the two bindings.rs reads and the arrlazy sabotage seed (PR-B), and for the signatures.rs read (P2).
…ver 1, PR-C2), wip
# Conflicts: # crates/perry-codegen/src/expr/index_get/array_rep.rs # crates/perry-wpfacts/src/lib.rs # crates/perry-wpfacts/src/model.rs # crates/perry-wpfacts/src/tests/array_reps.rs # crates/perry/src/commands/compile/compile_env.rs # crates/perry/src/commands/compile/compile_env/tests.rs # crates/perry/src/commands/compile/program_facts.rs # scripts/test-verify-facts-s0.py
# Conflicts: # crates/perry-wpfacts/src/dump.rs
|
Caution Review failedThe pull request is closed.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DESIGN-p4l1 rev 2 §3 (the field's rep,
FactsField.rep,FieldProof.arrayas the join over the class set), §4.2 (the one tier, here for field reads and for b1,this.at(sq)=this.board[sq] ?? 0behind a method), §2.8 (b1 stays export-time), §5.1-5.6 and §8's PR-C3 row, with REVIEW-p4l1's conditions for C3: finding 8 (hard.is_none(), join not meet, both already in PR-B'sFieldFacts::rep), finding 13 (the sites that are not in the floor) and "the passed-pawn interval from the dump first".Branch head:
3e926635b5. The branch merges currentorigin/main(482d9c9, with C2 #14394, P3 B3 #14395 and N6 #14396), by merge, not rebase; it merges cleanly with ba9234e.What changes
The field's rep reaches codegen (
FactsField.rep, from wpfactsFieldFacts::rep, which PR-B computed with no consumer). A field hasArrwhen it is definite, a data field, its template is neither escaped nor hard, and every value its field node holds, over every token of the template, is a plain program Array.FieldProof.arrayis the join of that rep over the receiver's class set,Nonewhen some class's field is notArr, and for a guarded or possibly absent read.wpfacts_access::field_arrayis the same join for a read however it is lowered (the field's value does not depend on the slot proof): an unchecked proven receiver, no interpreted code.One query for a receiver's rep (
proven_array::receiver_rep): a local, parameter or module constant (BindingFacts::rep, C2), or a field read (FieldProof.array). A directthis.f[i]read then takes C2's tier unchanged: the key's interval at the site (element_keys) against the floor, the number test bynumber_element_reads.b1 inside the floor (§2.8). The method body's read joins every caller's key, so its own facts keep the bound, the hole test and the
??. wpfacts'FieldReadCallgains:array:arr_repover every copy (inlined, and one per summarized instance) of the body's element-read receiver, which is what the read reads from;in_floor:array.dense, and this call's own key interval (call_arg_ranges) inside[0, array.floor).It is computed at export from the template's
hole_freebit (PR-B's fused fill), the length floor and the call's key. It does not go through C1'sElemUndef, so it holds for an array that is stored into.The in-floor call in codegen (
lower_call/field_read_call.rs). When the call is in the floor and the field read isArr, dense, with a floor, the call is: the index as an int32, the field load, and the tier's in-floor read (proven_array::lower_in_floor): oneand, the forwarding test when the storage can grow, the load ath + 24 + 8*i. With Number elements (rep.elemis F64 or I32) the value is never nullish and?? Lis the value. With Boxed elements and a?? Lthe call keeps the read it had (a storednullorundefinedis a present element).Not in the floor (make's
at(captureSquare),set'sat(square)): the rebuilt readthis.f[k] ?? Lgoes through the tier as any field read does: forwarding test,idx <u capacity, load, hole compare,??, cold arm.!fixed!fixedidx <u capacityidx <u ek_prefix, then lengthh + 24 + 8*idxh + 24 + 8*idx(floor > 0)TAG_HOLEcompare?? LThe per-site verdicts (the dump; chess.ts)
PERRY_FACTS_DUMPprints one line per field read call:# field_read_call module= call= field= key=[lo,hi] rep= in_floor=. Chess has 35 such calls, all onChess.board = arr<f64,128,dense>(growable: the store keys reach 255, so the forwarding test stays). 27 are in the floor, 8 are not:at(source)[0,127]at(to),at(capture)[0,127]at(home±k)[1,113]…[6,118]at(from)[0,127]at(moveTo(move))[0,127][0,127]at(captureSquare)[-16,143]at(square)[-128,255]at(to + direction)[-16,143]at(r*16+f)[0,255][0,119]": it does notat(square)[0,255]at(from), 741at(square)[0,255]The passed-pawn interval is settled:
[0,255], not in the floor, so evaluate is at the low end of the design's range, and below it, because two more site classes the design counted in the floor are not. Both are the range analysis, not this PR:r*16 + fwithr, fin[0,7]: the sum of two operands that both grow during the solve is widened past its hull. A probe with one method per shape (same binary, same dump) gives:at(r*16)[0,112](exact);at(r + f)[0,127](hull[0,14]);at(r*16 + f)[0,255](hull[0,119]) when both loops count up, and[0,119](exact) whenrcounts down from 7. The rungs aremodel::range::widen's ladder (7, 127, 255). Which growth takes the sum past 127 is not investigated.for (let square = 0; square < 128; square++) { if ((square & 0x88) !== 0) { square += 7; continue; } … this.at(square):gen/narrow.rsends the narrowed reads at the body's first write of the counter in program text, also when that write is in a branch that always leaves (continue), so the read after theifsees the counter's whole interval,[0,255]. The same probe: withsquare += 7; continue;the key is[0,255], with a barecontinue;it is[0,127].Those reads take the outside-the-floor form (saving about half of the in-floor form). Follow-up, with its own census: see below.
Check mode, switch, sabotage
PERRY_VERIFY_FACTS=1.array: Some(r)tests the word it loaded (array_rep::witness_value,Fact::ArrayRep): heap band,GC_TYPE_ARRAY, not forwarded whenfixed,length >= floorandcapacity >= floor.array_rep::witness_read): the claims above, no element descriptor, front offset 0, the key is below the call's floor, the slot is no hole and holds an element ofrep.elem. b1's own witness stays: the real call runs beside the read and the two values must be the same bits.PERRY_FACTS_ARRAY_FIELDS=0:FieldFacts::repis Boxed and no call is in the floor, which is C2's build (before the merge of main, the off arm's chess counts are C2's on arm to 0.1 M). It is a compile-cache input (compile_env.rs), andarray_fact_switches_are_cache_inputslists it.arrfield: a field's rep ignores thenull/undefinedits node also holds (array-field-negative.ts: the witness at the field read reports thenullword);arrb1: every call into a dense floor is in the floor, whatever its key (array-b1-negative.ts: keysep - 16andep + 16on a 16-element board; the tier's witness reports the key).Results (qb9,
instructions:u)Build
9b04629446(the head's code: the three later commits change two unit-test files and a comment in a parity fixture). On vsPERRY_FACTS_ARRAY_FIELDS=0on the same binary, two runs each, checksums equal in every run.Runs: search 30 754.3 / 30 754.4 on, 33 147.9 twice off; the other three identical between runs. Before the merge of main (build
8766d29dbe, without P3 B3 and N6): search 31 998.9 / 34 341.2 (−6.82%), perft 747.0 / 807.6 (−7.50%), evaluate 2 870.1 / 3 117.5 (−7.94%), legal 1 110.7–1 111.1 / 1 167.5 (−4.87%); that off arm equals C2's on arm to 0.1 M.Search, perft and legal are at 79%, 86% and 87% of the design's low estimate in instructions; evaluate is at 58% of it. The gap is the interval verdicts above: with evaluate:342/354/369 outside the floor the design's own per-read numbers give 0.606 M × 16 + 39.59 M × 8 = 326 M for evaluate, against 247.5 M measured.
The board read in
attacked(this.at(source) === color * 2after the knight delta), from the disassembly of both arms: with the fact off the hot path is 20 instructions up to the load (field load, band, type word,ek_prefixbound, slot math, load), followed by the??test; with it on it is 9, and there is no??:Chess.attacked$sigis 986 instructions with the fact on and 1 169 off. Tier blocks per function (on / off), read from the IR:attacked7 in-floor reads and no??(off: 7??, 14 guard chains);make2 forwarding tests, 1 capacity bound, 1 hole test, 1??(at(from)in the floor,at(captureSquare)not; off: 2??, 4 guard chains);generatePseudo15 reads, 3 of them bounded (off: 30 guard chains, 15??).for_of_literal,copy_holes_facts) × on, fields off, reps off, check mode,PERRY_GC_MOVING_LOOP_POLLS=1,PERRY_GC_STRESS=7: 108 of 108 MATCHPERRY_VERIFY_FACTS=1test_gap_s8b_computed_fields,test_gap_10554_fn_identity_own_module)array-field-negativeandarray-b1-negative: correct 0, false 86 ("array rep");array-dense/elem/lazy/front/undef-negativefalse arms 863e926635b5: perry-codegen 2850 passed, perry-wpfacts 2221 passed, 0 failed (the build commit's run had one wrong test expectation in each, fixed in the head). perry-hir 881 passed; perry-wpfactsnum_seeds_audit2 passed (no newprim::NUMuse); perry-runtime 6641 passed, 0 failed. perrycompile_env4 passed (witharray_fact_switches_are_cache_inputs), 1 failed:codegen_env_vars_are_build_cache_inputs("shared list must be sorted and unique"), the failure main already has (FOLLOWUPS.md)-D warningsscripts/check_file_size.shSize (every executable section; on vs off)
.text,perry_js_text,.init,.plt,.plt.got,.fini..textaloneperry_js_textaloneNo section of any program grows. Per symbol: skin's symbol table is identical on and off; tssrc differs in one function (
addMissingAwait.tsgetDeclarationSiteFix, 2 964 B on against 3 083 B off); Supabase differs in one closure (functions/new/new.handler.tsclosure 31, 42 815 B on against 42 808 B off: +7 B, not looked into, inside the section's padding since the section sizes are equal).Departures from the design, and what is not done
??through b1's export-timein_floor, not throughElemUndef. C2's departure stands for direct reads: C1'sElemUndeftakes the hole'sundefinedfromhole(t), which every index store marks, so a direct read of a stored-into array (whitePawns[file], and a directthis.board[i]) is Number-or-undefined in the facts and keeps its??and number test. Chess reads its board only throughat, so that does not cost C3 anything. The solver change (a store of a non-undefinedvalue into a slot below the dense floor creates no hole) is not in this PR.rep.elem, notnumber_element_reads(C2's rule for direct reads). The body's read node holds theundefinedof the other callers' keys, sonumber_element_readscan never say Number for it. It is the same solver fact (the element nodes hold only Numbers), tested per read by the witness's element test, and P3 B3 is merged. Direct reads keep C2's rule.FieldReadCall.in_floorreaches codegen as the floor (FactsFieldReadCall.in_floor: Option<u32>), not as a bool. The floor wpfacts proved the key below is over the Arrays the method's read can read; codegen'sFieldProof.arrayis over the receiver binding's classes. They agree in every program measured, but the witness tests the key against the floor the claim was made for.FieldReadCall.arrayis the rep of the method's read receiver, notFieldFacts::repof the call's receiver classes: wpfacts has the receiver node there, not codegen's class set. Codegen still requiresFieldProof.array(with finding 8'shardtest) for the same read.?? Lkeeps the read it had (bound, hole test,??). A reader without a default (return this.f[k]) is in the floor for any element kind.this.at(this.ep - 16)) is not a b1 call in codegen (b1's admission wants an inert index: main's rule), so wpfacts' in-floor verdict for it has no consumer.at(from). Not measured.this[name] = other) and the negative controls (storedundefined, anullfield, no fused fill, a sparse store past the end,deleteof an element, a length store, a deleted field, an overriding subclass, string elements, fraction/NaN/-0keys) aretest_array_reps_fields.ts; 13 is…_fields_gc.ts; 14 is…_fields_growth.ts(push in the reading loop, and one store far past the capacity between reads).proven_array_tests.rs: the in-floor call of a growable and of a fixed board, the call outside the floor, the direct field read, a Boxed field changes nothing, the join). The chess claims above are read from its IR and disassembly by hand.cargo check --testsran there on an early commit and stopped at a test-only error in perry-wpfacts before reaching perry-codegen; nothing later was compiled on the Mac.